公式チュートリアルでNext.jsに入門してみた (3) 〜Dynamic Routes、API Routes編〜
こんにちは、CX事業本部 IoT事業部の若槻です。
フロントエンドフレームワークNext.jsへの入門のために、次の公式チュートリアルを数回のシリーズに分けてこなしていき、基本的な機能に触れていこうと思います。
やってみた
本記事(3)では、チュートリアルのうち「Dynamic Routes」および「API Routes」をやっていき、Next.jsのDynamic RoutesおよびAPI Routesについて触れていきます。
- (1) 〜アプリ新規作成、ページ遷移、スタイリング編〜
- Create a Next.js App
- Navigate Between Pages
- Assets, Metadata, and CSS
- (2) 〜Pre-rendering、データフェッチ編〜
- Pre-rendering and Data Fetching
- (3) 〜Dynamic Routes、API Routes編〜
- Dynamic Routes
- API Routes
- (4) 〜デプロイ編〜
- Deploying Your Next.js App
Dynamic Routes
前回までの実装では外部データを使用してブログのインデックスページを作成しました。
ここではDynamic Routesを使用してブログページへのパスを作成します。
Implement getStaticPaths
まず、pages/posts/first-post.js
ファイルはこの後の手順では不要なので削除します。
次に、lib/posts.js
ファイルの末尾に以下の記述を追記します。posts
ディレクトリ配下のファイル名一覧を返すgetAllPostIds
関数です。
export function getAllPostIds() { const fileNames = fs.readdirSync(postsDirectory) // Returns an array that looks like this: // [ // { // params: { // id: 'ssg-ssr' // } // }, // { // params: { // id: 'pre-rendering' // } // } // ] return fileNames.map(fileName => { return { params: { id: fileName.replace(/\.md$/, '') } } }) }
page
ディレクトリ配下にposts
ディレクトリを作成します。
pages/posts
配下に次の通り[id].js
ファイルを作成します。この[
で始まり]
で終わるページにより、リクエストされたURLに応じてにより動的にページが返されるようになります。これがDynamic Routesです。
import Layout from '../../components/layout' import { getAllPostIds } from '../../lib/posts' export async function getStaticPaths() { const paths = getAllPostIds() return { paths, fallback: false } } export default function Post() { return <Layout>...</Layout> }
このgetStaticPaths
では、次の形式でpaths
を返すことにより、どのパスでPre-renderingがされるのかが決定されるようになります。
return { paths: [ { params: { id: '1' } }, { params: { id: '2' } } ], fallback: ... }
Implement getStaticProps
ここではid
をもとにブログポストをレンダリングできるようにしていきます。
lib/posts.js
ファイルの末尾に次の記述を追記します。getPostData
関数はブログのポストデータをid
をもとに返します。
export function getPostData(id) { const fullPath = path.join(postsDirectory, `${id}.md`) const fileContents = fs.readFileSync(fullPath, 'utf8') // Use gray-matter to parse the post metadata section const matterResult = matter(fileContents) // Combine the data with the id return { id, ...matterResult.data } }
pages/posts/[id].js
ファイルで次の行の記述を、
import { getAllPostIds } from '../../lib/posts'
次の記述で置き換えます。
import { getAllPostIds, getPostData } from '../../lib/posts' export async function getStaticProps({ params }) { const postData = getPostData(params.id) return { props: { postData } } }
これによりgetStaticProps
内のgetPostData
関数がブログのポストデータをpropsとして返すようになります。
そしてpages/posts/[id].js
ファイル内のPost
を次の記述で置き換えます。
export default function Post({ postData }) { return ( <Layout> {postData.title} <br /> {postData.id} <br /> {postData.date} </Layout> ) }
ブログポストのURLにアクセスしたらそれぞれのページを開けるようになりました!
http://localhost:3000/posts/ssg-ssr
http://localhost:3000/posts/pre-rendering
Render Markdown
マークダウンのコンテンツをレンダリングするためにremark
ライブラリをインストールします。
$ npm install remark remark-html
lib/posts.js
ファイルの冒頭に次の記述を追記します。
import { remark } from 'remark' import html from 'remark-html'
同じくlib/posts.js
ファイルでgetPostData()
関数を次の通り更新します。
export async function getPostData(id) { const fullPath = path.join(postsDirectory, `${id}.md`) const fileContents = fs.readFileSync(fullPath, 'utf8') // Use gray-matter to parse the post metadata section const matterResult = matter(fileContents) // Use remark to convert markdown into HTML string const processedContent = await remark() .use(html) .process(matterResult.content) const contentHtml = processedContent.toString() // Combine the data with the id and contentHtml return { id, contentHtml, ...matterResult.data } }
ここではremark
をawaitで実行できるように、getPostData()
をasync関数としています。
よってpages/posts/[id].js
ファイルのgetStaticProps
で、getPostData
をawaitで実行するように更新します。
import Layout from '../../components/layout' import { getAllPostIds, getPostData } from '../../lib/posts' export async function getStaticProps({ params }) { const postData = await getPostData(params.id) return { props: { postData } } }
さらにpages/posts/[id].js
ファイルのPost
コンポーネントを次のように更新します。
export default function Post({ postData }) { return ( <Layout> {postData.title} <br /> {postData.id} <br /> {postData.date} <br /> <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} /> </Layout> ) }
dangerouslySetInnerHTML
を使用してcontentHtml
をレンダリングすることにより、Reactから安全にHTMLを生成できます。(dangerously
と付いているのは、コードからHTMLを生成するのはXSSの脆弱性を生む危険があることを認識させるためだそうです。)
ここでブログポストのURLにアクセスしたら次のようなエラーとなっていました。
前回と同じエラーですね。またnpm upgrade
して開発サーバーを再起動します。
するとそれぞれのページを正常に開けました。マークダウンから変換されたコンテンツも見れています!
http://localhost:3000/posts/ssg-ssr
http://localhost:3000/posts/pre-rendering
Polishing the Post Page
ブログポストのページを改善します。
Adding title to the Post Page
ここではポストページにブログタイトルを付けるようにします。
pages/posts/[id].js
ファイルの冒頭に次の記述を追加します。
import Head from 'next/head'
さらにpages/posts/[id].js
ファイルで、Post
Componentで次のように{postData.title}
を<title>
と<Head>
で囲みます。
export default function Post({ postData }) { return ( <Layout> <Head> <title>{postData.title}</title> </Head>
Formatting the Date
ここでは、ブログの日付のフォーマットを行います。
date-fns
をインストールします。(必要に応じて再度npm upgrade
も実行します。)
$ npm install date-fns
components
ディレクトリ配下に次の内容でdate.js
ファイルを作成します。
import { parseISO, format } from 'date-fns' export default function Date({ dateString }) { const date = parseISO(dateString) return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time> }
pages/posts/[id].js
ファイルに次の記述を追加します。
import Date from '../../components/date'
pages/posts/[id].js
ファイルで、Post
Componentを次のように{postData.date}
をDate
を使用するようにします。
export default function Post({ postData }) { return ( <Layout> <Head> <title>{postData.title}</title> </Head> <br /> <Date dateString={postData.date} />
http://localhost:3000/posts/pre-renderingにアクセスすると、日付の形式をフォーマットできていますね!
Adding CSS
ここでは、ポストページをCSSによりスタイリングします。
pages/posts/[id].js
ファイルに次の記述を追加します。
import utilStyles from '../../styles/utils.module.css'
pages/posts/[id].js
ファイルで、Post
Componentを次のように更新します。
export default function Post({ postData }) { return ( <Layout> <Head> <title>{postData.title}</title> </Head> <article> <h1 className={utilStyles.headingXl}>{postData.title}</h1> <div className={utilStyles.lightText}> <Date dateString={postData.date} /> </div> <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} /> </article> </Layout> ) }
http://localhost:3000/posts/pre-renderingにアクセスすると、タイトルと日付のスタイリングができていますね!
Polishing the Index Page
インデックスのページを改善します。
pages/index.js
ファイルの冒頭に次の記述を追記します。
import Link from 'next/link' import Date from '../components/date'
pages/index.js
ファイルのHome
Componentで、<li>
の内容を次の通り更新します。
export default function Home({ allPostsData }) { return ( <Layout home> {/* Keep the existing code here */} {/* Add this <section> tag below the existing <section> tag */} <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}> <h2 className={utilStyles.headingLg}>Blog</h2> <ul className={utilStyles.list}> {allPostsData.map(({ id, date, title }) => ( <li className={utilStyles.listItem} key={id}> <Link href={`/posts/${id}`}> <a>{title}</a> </Link> <br /> <small className={utilStyles.lightText}> <Date dateString={date} /> </small> </li> ))} </ul> </section> </Layout> ) }
http://localhost:3000/にアクセスすると、インデックスで各ブログポストへのリンクと日付を表示させられました!
API Routes
Creating API Routes
API Routesを使用することにより、Next.jsアプリケーション内にAPIエンドポイントを作成することができます。
pages
ディレクトリ配下にapi
ディレクトリを作成します。
pages/api
ディレクトリ配下に、hello.js
ファイルを次の通り作成します。
export default function handler(req, res) { res.status(200).json({ text: 'Hello' }) }
http://localhost:3000/api/helloにアクセスすると、APIエンドポイントで取得した内容が表示できました!
Next.jsのフレームワーク単体でフロントエンドとバックエンドAPIの実装ができるというのは便利ですね!
おわりに
公式チュートリアルでNext.jsに入門してみた (3) 〜Dynamic Routes、API Routes編〜 でした。
次回はこちらです。
以上